package edu.washington.tcss360.burningpenguins.client;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;

import edu.washington.tcss360.burningpenguins.data.DataObject;
import edu.washington.tcss360.burningpenguins.database.BPDatabaseConnection;
import edu.washington.tcss360.burningpenguins.queries.QueryBy;
import edu.washington.tcss360.burningpenguins.util.DataObjectField;
import edu.washington.tcss360.burningpenguins.util.DataObjectFieldDataType;

/**
 * A base class for implementing the logic used in operations on the database
 * tables.  This class makes use of reflection to avoid redundant implementations
 * in each of the clients.
 * 
 * @author Chris Gelatt
 * @version 1.0
 * @since 2013-11-2
 *
 */
public class DataObjectClient
{
	protected String tableName;
	protected List<QueryBy> validQueries;
	protected List<DataObjectField> objectFields;
	protected String classPath;
	private BPDatabaseConnection connection;
	
	public DataObjectClient(BPDatabaseConnection connection)
	{
		tableName = "";
		validQueries = null;
		objectFields = null;
		this.connection = connection;
		classPath = "";
	}
	
	/**
	 * This method is called by the get() method of each child class.
	 * It retrieves the object in the associated table by ID and returns
	 * it.  The child class then casts this object to the actual class it
	 * should be.
	 * @param id The ID of the object to retrieve.
	 * @return A DataObject object to be cast to its actual class by the
	 * calling method.
	 */
	protected DataObject getBase(Integer id)
	{
		try
		{
			ResultSet result = connection.executeQuery("SELECT * FROM " + tableName + " WHERE Id = " + id);
			
			//If we have a result, build an object to return.
			if(result.next())
			{
				//Instantiate a new object of the child class
				Object returnObj = Class.forName(classPath).newInstance();
				
				//Call the setId() method on the child class
				Method objIdMethod = returnObj.getClass().getDeclaredMethod("setId", Integer.class);
				objIdMethod.invoke(returnObj, result.getInt("Id"));
				
				//Loop through and populate each of the remaining fields on the child class
				for(DataObjectField tempField : objectFields)
				{
					//The getReturnType() method on the JDBC library is apparently not public,
					//so this enum is a workaround for that.
					switch(tempField.getRawDataType())
					{
						case INTEGER:
							Method integerObjMethod = returnObj.getClass().getDeclaredMethod("set" + tempField.getTitle(), Integer.class);
							integerObjMethod.invoke(returnObj, result.getInt(tempField.getColumnTitle()));
							break;
						case LONG:
							Method longObjMethod = returnObj.getClass().getDeclaredMethod("set" + tempField.getTitle(), Long.class);
							longObjMethod.invoke(returnObj, result.getLong(tempField.getColumnTitle()));
							break;
						case STRING:
							Method stringObjMethod = returnObj.getClass().getDeclaredMethod("set" + tempField.getTitle(), String.class);
							stringObjMethod.invoke(returnObj, result.getString(tempField.getColumnTitle()));
							break;
						default:
							break;
					}
				}
				
				//We know we just built something in the DataObject tree, so casting it to that
				//is a safe operation.
				return (DataObject)returnObj;
			}
			
			//If we didn't have a result, we return null
			return null;
		}
		catch(SQLException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(ClassNotFoundException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(IllegalAccessException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(InstantiationException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(NoSuchMethodException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(InvocationTargetException e)
		{
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * This method is called by the getAll() method of each child class.
	 * It retrieves all objects that match the queries in the array passed as
	 * a parameter and returns the results as an array of objects.  The child
	 * class then casts these objects to the actual class they should be.
	 * @param dbQueries The QueryBy queries to use to retrieve matching objects from the database.
	 * @return An array of DataObject objects to be cast to their actual class by the
	 * calling method.
	 */
	protected DataObject[] getAllBase(QueryBy[] dbQueries)
	{
		try
		{
			String dbQueryString = "";
			
			//Allow for null to be passed to just get everything in a table.
			if(dbQueries == null || dbQueries.length == 0)
			{
				dbQueryString = "SELECT * FROM " + tableName;
			}
			else
			{
//TODO - Implement type safety check
/*				if(!validQueries.contains(dbQueries[0]))
				{
					return null;
				}*/
				
				//Build the query string
				dbQueryString = "SELECT * FROM " + tableName + " WHERE " + dbQueries[0].toString();
				if(dbQueries.length > 1)
				{
					for(int i = 1; i < dbQueries.length; i++)
					{
//TODO - More type safety checks
/*						if(!validQueries.contains(dbQueries[i]))
						{
							return null;
						}*/
						
						dbQueryString += " AND " + dbQueries[i].toString();
					}
				}
			}

			ResultSet result = connection.executeQuery(dbQueryString);
			List<DataObject> returnList = new LinkedList<>();
			
			//Build a DataObject for each row we got back
			while(result.next())
			{
				//Create the object
				Object returnObj = Class.forName(classPath).newInstance();
				
				//Call the setId() method on the object
				Method objIdMethod = returnObj.getClass().getDeclaredMethod("setId", Integer.class);
				objIdMethod.invoke(returnObj, result.getInt("Id"));

				//Loop through and set each field on the object
				for(DataObjectField tempField : objectFields)
				{
					//The getReturnType() method on the JDBC library is apparently not public,
					//so this enum is a workaround for that.
					switch(tempField.getRawDataType())
					{
						case INTEGER:
							Method integerObjMethod = returnObj.getClass().getDeclaredMethod("set" + tempField.getTitle(), Integer.class);
							integerObjMethod.invoke(returnObj, result.getInt(tempField.getColumnTitle()));
							break;
						case LONG:
							Method longObjMethod = returnObj.getClass().getDeclaredMethod("set" + tempField.getTitle(), Long.class);
							longObjMethod.invoke(returnObj, result.getLong(tempField.getColumnTitle()));
							break;
						case STRING:
							Method stringObjMethod = returnObj.getClass().getDeclaredMethod("set" + tempField.getTitle(), String.class);
							stringObjMethod.invoke(returnObj, result.getString(tempField.getColumnTitle()));
							break;
						default:
							break;
					}

				}
				
				//Add the object to the list.  Since we know we just built
				//a DataObject, this cast is safe.
				returnList.add((DataObject) returnObj);
			}
			
			//Return what we have
			return returnList.toArray(new DataObject[returnList.size()]);
		}
		catch(SQLException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(ClassNotFoundException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(IllegalAccessException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(InstantiationException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(NoSuchMethodException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(InvocationTargetException e)
		{
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Creates an object in the associated table based on the actual class of the object.
	 * @param createObject The object to persist to the database.
	 * @return The ID of the newly-created object.  Note that this is not a thread-safe
	 * operation, so it would need to be modified if heavy parallel use were to happen.
	 */
	public Integer create(DataObject createObject)
	{
		try
		{
			//Make sure we're not missing any required fields
			for(DataObjectField tempField : objectFields)
			{
				if(tempField.getRequired())
				{
					Method objMethod = createObject.getClass().getDeclaredMethod("get" + tempField.getTitle());
					if(objMethod.invoke(createObject) == null)
					{
						return null;
					}
				}
			}
			
			//Start building the statement string
			String insertStatement = "INSERT INTO " + tableName + " VALUES (null, ";
			
			//Loop through and append values for each field in the object
			//along with syntax to keep the statement legal
			for(int i = 0; i < objectFields.size(); i++)
			{
				Method objMethod = createObject.getClass().getDeclaredMethod("get" + objectFields.get(i).getTitle());
				Object fieldValue = objMethod.invoke(createObject);
				if(fieldValue == null)
				{
					insertStatement += "null";
				}
				else
				{
					if(objectFields.get(i).getDataType().equals(DataObjectFieldDataType.STRING.toString()))
					{
						insertStatement += "'";
					}
					insertStatement += fieldValue.toString();
					if(objectFields.get(i).getDataType().equals(DataObjectFieldDataType.STRING.toString()))
					{
						insertStatement += "'";
					}
				}
				
				if(i < (objectFields.size() - 1))
				{
					insertStatement += ", ";
				}
			}
			
			insertStatement += ");";
		
			//Actually execute the statement
			connection.executeUpdate(insertStatement);
			
			//Note: This is NOT thread safe
			return connection.executeQuery("SELECT Id FROM " + tableName + " ORDER BY Id DESC LIMIT 1;").getInt("Id");
			
		}
		catch(SQLException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(IllegalAccessException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(NoSuchMethodException e)
		{
			e.printStackTrace();
			return null;
		}
		catch(InvocationTargetException e)
		{
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 * Persists an array of objects to the database using the create() method.
	 * 
	 * @param createObjects An array of objects to persist to the database.
	 * @return The IDs of the new objects.
	 */
	public Integer[] create(DataObject[] createObjects)
	{
		List<Integer> returnList = new LinkedList<>();
		for(DataObject tempObject : createObjects)
		{
			returnList.add(create(tempObject));
		}
		
		return returnList.toArray(new Integer[returnList.size()]);
	}
	
	/**
	 * Updates non-null and non-zero fields on an object.  Requires the
	 * ID of the object to have been set.
	 * 
	 * @param updateObject The object with changes that are to be persisted
	 * to the database.  Any null or zero-value fields are skipped.
	 */
	public void update(DataObject updateObject)
	{
		//The ID numbering in the database starts at 1,
		//and if the object that was passed in doesn't
		//have an ID set, it defaults to 0.
		if(updateObject.getId() < 1)
		{
			return;
		}
		
		try
		{
			//Start building the statement
			String updateStatement = "UPDATE " + tableName + " SET ";
			Integer updateStatementBaseLength = updateStatement.length();
			
			//Loop through and add in any non-null, non-zero values
			//to the update statement
			for(int i = 0; i < objectFields.size(); i++)
			{
				Method objMethod = updateObject.getClass().getDeclaredMethod("get" + objectFields.get(i).getTitle());
				Object fieldValue = objMethod.invoke(updateObject);
				if(fieldValue != null)
				{
					if(updateStatement.length() > updateStatementBaseLength)
					{
						updateStatement += ", ";
					}
					updateStatement += objectFields.get(i).getColumnTitle() + "=";
					if(objectFields.get(i).getDataType().equals(DataObjectFieldDataType.STRING.toString()))
					{
						updateStatement += "'";
					}
					updateStatement += fieldValue.toString();
					if(objectFields.get(i).getDataType().equals(DataObjectFieldDataType.STRING.toString()))
					{
						updateStatement += "'";
					}
				}
				
			}
			
			updateStatement += " WHERE Id=" + updateObject.getId() + ";";
			
			//Execute the statement
			connection.executeUpdate(updateStatement);
			
			return;
		}
		catch(IllegalAccessException e)
		{
			e.printStackTrace();
			return;
		}
		catch(NoSuchMethodException e)
		{
			e.printStackTrace();
			return;
		}
		catch(InvocationTargetException e)
		{
			e.printStackTrace();
			return;
		}		
	}
	
	/**
	 * Updates an array of objects in the database with the update() method.
	 * @param updateObjects The objects with changes to persist to the database.
	 */
	public void update(DataObject[] updateObjects)
	{
		for(DataObject tempObject : updateObjects)
		{
			update(tempObject);
		}

		return;
	}
	
	/**
	 * Deletes an object with the specified ID.
	 * @param id The ID of the object to delete.
	 */
	public void delete(Integer id)
	{
		connection.executeUpdate("DELETE FROM " + tableName + " WHERE Id=" + id);
		return;
	}
	
	/**
	 * Deletes multiple objects by ID using the delete() method.
	 * @param ids An array of IDs of objects to delete.
	 */
	public void delete(Integer[] ids)
	{
		for(Integer tempId : ids)
		{
			delete(tempId);
		}
		
		return;
	}
	
	/**
	 * Deletes an object passed in.  Requires the ID on the object to be set.
	 * @param deleteObject The object to delete from the database.
	 */
	public void delete(DataObject deleteObject)
	{
		if(deleteObject.getId() > 0)
		{
			delete(deleteObject.getId());
		}

		return;
	}
	
	/**
	 * Deletes an array of objects passed in.  Uses the delete() method to do this.
	 * @param deleteObjects The array of objects to delete from the database.
	 */
	public void delete(DataObject[] deleteObjects)
	{
		for(DataObject tempObject : deleteObjects)
		{
			delete(tempObject);
		}
		
		return;
	}
}
